/*
 * Copyright 2007, 2008 Tiberiumlabs
 *
 * This file is part of Tiberiumlabs Lailaps.
 *
 * Tiberiumlabs Lailaps is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tiberiumlabs Lailaps is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.tiberiumlabs.lailaps.downloads;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tiberiumlabs.lailaps.Mediator;
import org.tiberiumlabs.lailaps.actions.events.DataEvent;
import org.tiberiumlabs.lailaps.actions.events.DataListener;
import org.tiberiumlabs.lailaps.list.DownloadItem;
import org.tiberiumlabs.lailaps.list.DownloadsListModel;
import org.tiberiumlabs.lailaps.list.StatusConstants;
import org.tiberiumlabs.lailaps.settings.NetworkSettings;
import org.tiberiumlabs.lailaps.settings.Settings;

/**
 *
 * @author <a href="paranoid.tiberiumlabs@gmail.com">Paranoid</a>
 */
public class DownloadManager implements DataListener {

    static Log LOG = LogFactory.getLog(DownloadManager.class);
    private final PriorityQueue<ListItem> itemsQueue = new PriorityQueue<ListItem>();
    private final NetworkSettings networkSettings = Settings.getNetworkSettings();

    private Object lock = new Object();
    private ArrayList<DownloadThread> runningThreads;
    private int running = 0;

    public final void add(DownloadItem item, int row) {
        ListItem listItem = new ListItem(item, row);
        synchronized (itemsQueue) {
            itemsQueue.offer(listItem);
        }
        mayBeRun();
    }

    public void remove(int row) {
        synchronized (itemsQueue) {
            itemsQueue.remove(new ListItem(null, row));
        }
        stop(row);
    }

    public void pause(int row) {
        setNeedStatus(row, StatusConstants.PAUSED);
    }

    public void stop(int row) {
        setNeedStatus(row, StatusConstants.STOPPED);
    }

    /**
     * 
     * @param row row from table to change status
     * @param status new status, from StatusConstants.
     * @return <code>true</code> if running element with specified row
     * found, <code>false</code> otherwise.
     */
    private boolean setNeedStatus(int row, int status) {
        for (int i = 0; i < runningThreads.size(); i++) {
            DownloadThread downloadThread = runningThreads.get(i);
            if (downloadThread.getRow() == row) {
                downloadThread.setNeedStatus(status);
                return true;
            }
        }
        return false;
    }

    /**
     * Checks is there is free slots to run one more download thread.
     * If there is - incrementing couner and adding new thread to list
     * of currently running thread (need it for DataListener olny)
     */
    private final void mayBeRun() {
        int maxRunning = networkSettings.getMaxWorkingThreads();
        synchronized (lock) {
            if (running == maxRunning) {
                return;
            }
            while (running < maxRunning) {
                ListItem item;
                synchronized (itemsQueue) {
                    item = itemsQueue.poll();
                }
                final DownloadThread downloadThread = new DownloadThread(item.getItem(), item.getRow());
                execute(new Runnable() {

                    public void run() {
                        runDownload(downloadThread);
                    }

                });
                running++;
                runningThreads.add(downloadThread);
            }
        }
    }

    /**
     * decrement running count and remove from list of currently running,
     * than checking may be have slots to run another threads.
     * @param downloadThread
     */
    private final void done(DownloadThread downloadThread) {
        synchronized (lock) {
            running--;
            runningThreads.remove(downloadThread);
        }
        mayBeRun();
    }

    // <editor-fold defaultstate="collapsed" desc=" executor handling ">
    private ExecutorService executor;
    private Object executorLock = new Object();

    public final void shutdown() {
        if (executor == null) {
            return;
        }
        executor.shutdown();
        try {
            executor.awaitTermination(10, TimeUnit.SECONDS);
        } catch (Exception ex) {
        }
        LOG.info("[ DOWNLOADS ] Executor shutted down.");
    }

    /**
     * This method runs all the downloads pool. It does no calculations about
     * currently running threads, because it's also responsible for
     * segments of download.
     * Quantity of currently running downloads is performed in other methods of
     * this class.
     * @param runnable item to run.
     */
    void execute(Runnable runnable) {
        synchronized (executorLock) {
            if (executor == null) {
                int threadsCount = networkSettings.getMaxWorkingThreads() * networkSettings.getMaxSegmentsPerThread();
                runningThreads = new ArrayList<DownloadThread>(threadsCount + 2);
                executor = Executors.newFixedThreadPool(threadsCount, new DownloadThreadFactory());
                LOG.info("[ DOWNLOADS ] Thread pool executor created.");
            }
            executor.execute(runnable);
        }
    }

    private void runDownload(DownloadThread downloadThread) {
        try {
            // TODO process all the download
        } catch (Exception ex) {
            LOG.warn("[ DOWNLOADS ] Exception in DownloadManager worker", ex);
        } finally {
            done(downloadThread);
        }
    }

    // </editor-fold>

    public static final class ListItem implements Comparable<ListItem> {

        private final DownloadItem item;
        private int row;

        public ListItem(DownloadItem item, int row) {
            this.item = item;
            this.row = row;
        }

        public DownloadItem getItem() {
            return item;
        }

        public int getRow() {
            return row;
        }

        public void setRow(int row) {
            this.row = row;
        }

        public int compareTo(ListItem o) {
            return this.row - o.row;
        }

        @Override
        public boolean equals(Object obj) {
            try {
                ListItem i = (ListItem)obj;
                return row == i.row;
            } catch (Exception ex) {
            }
            return false;
        }

        @Override
        public int hashCode() {
            return row;
        }

    }

    private static final class DownloadThreadFactory implements ThreadFactory {

        private int priority = Thread.NORM_PRIORITY - 2;
        private int count = 1;
        private final ThreadGroup threadGroup;

        public DownloadThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            threadGroup = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        }

        public Thread newThread(Runnable r) {
            Thread thread = new Thread(threadGroup, r, "Lailaps download " + count++);
            if (!thread.isDaemon()) {
                thread.setDaemon(true);
            }
            if (thread.getPriority() != priority) {
                thread.setPriority(priority);
            }
            LOG.debug("[ DOWNLOADS ] New Download thread created.");
            return thread;
        }

    }

    public void dataChanged(DataEvent event) {
        long start = System.currentTimeMillis();
        if (LOG.isDebugEnabled()) {
            LOG.debug("[ DOWNLOADS ] Applying DataEvent: " + event.toString());
        }
        if (!runningThreads.isEmpty()) {
            for (int i = 0; i < runningThreads.size(); i++) {
                // TODO may be need to synchronize on list to do this
                runningThreads.get(i).dataChanged(event);
            }
        }
        if (!itemsQueue.isEmpty()) {
            DownloadsListModel model = Mediator.getMediator().getDownloadsListModel();
            Iterator<ListItem> iterator = itemsQueue.iterator();
            while (iterator.hasNext()) {
                ListItem item = iterator.next();
                item.row = event.calculateCurrentRow(item.row);
                if (item.row == -1 && item.item != null) {
                    item.row = model.indexOfItem(item.item);
                }
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("[ DOWNLOADS ] dataChanged done in " + (System.currentTimeMillis() - start) + " millis.");
        }
    }

}
